終於來到一個跟 FP 的核心概念有關的特性了,首先我們需要知道 Haskell 是具有 First-class function 這個特性,意思是指 function 可以把它當一般的變數來看,我們可以把 function 存在某個資料結構中、當作 function 回傳值、把變數當作參數等等跟變數一樣的行為。
簡單來說就是 function 當作 function 參數以及 function 回傳 function 這件事情。也就是說因為有 First-class function 這個特性我們才能這樣使用 function 。
在使用 Haskell 之前,這邊先用我比較熟悉的程式語言來說明這個特性
const add5 = (x) => x + 5;
const list = [1,2,3];
console.log(list.map(add5)) // [6,7,8]
我們這邊直接傳入 add5
當作 map
的參數,[list.map](http://list.map)
參數的 type 是 (a:number) => number
,所以我們可以直接傳入 add5
,如果沒有這個特性大概會變成是這樣寫
const add5 = (x) => x + 5;
const list = [1,2,3];
for(let i = 0; i<list.length ; i++){
list[i] = add5(list[i])
}
console.log(list) // [6,7,8]
當然這個範例的差異還有一個是 imuttable 而另一個是 muttable ,但我們先把重點放在 function 就好。
簡單來說明這個概念就是,對於多參數 function 來說,我們可以不用一次傳入所有的參數,如果沒有傳完會先回傳 function 直到所有的參數都傳入為止。
首先有一個概念在 Haskell 中所有 function 都是 curried function,也可以這麼說「在 Haskell 中所有函數都只有一個參數,然後也許再回傳一個 function 或者回傳一個數值」
add x y = x + y
add 3 5 -- 8
(add 3) 5 -- 8
這邊可以得知我直接傳入 3 5
跟先傳入 3
再傳入5
是一樣的,也就是(add 3 )
這個expression 是會回傳一個 function 。
但其實
add 3 5
也算是先傳入3
再傳入5
就是了
整個流程詳細解釋的話大概會像是這樣:
首先我們已經知道 add:: Num a => a -> a -> a
所以 (add 3 ) :: Num a => a -> a
那 (add 3) 5
就是顯而易見就是 :: Num a => a
那 curried function 的好處是什麼呢?我認為最大的用途我可以用一個 function 快速建構另外一個 function
add3 = add 3
add3 5 -- 8
add3 10 -- 13
運用這種特性可以讓我們更好的 reuse 或者組合 function。
那就算是 infix function 我們也是能夠不用一次傳入所有function
product5 = (*5)
prdouct5 10 -- 50
isUpperChar = (`elem`) ['A'..'Z']
isUpperChar 'a' -- Flase
isUpperChar 'B' -- True
我們可以用 ()
包住 function 這表示我要馬上呼叫他,首先一樣要是那個前提「在 Haskell 中所有函數都只有一個參數,然後也許再回傳一個 function 或者回傳一個數值」
(*) :: Num a => a -> a -> a
(*5) :: Num a ⇒ a → a
(*5) 10 :: Num a ⇒ a
綜合以上知識我們可以做出這些操作
transformStr :: (String -> String) -> [String] -> [String]
transformStr f s = case s of
[] -> []
(x:xs) -> f x : transformStr f xs
首先我們先宣告一個拿來處理字串的 function ,這裡可以看到他的 type 是transformStr :: (String -> String) -> [String] -> [String]
這裡會看到一個新的型別 (String -> String)
為什麼突然會需要 ()
?因為這樣我們這樣才知道這是在描述一個 function 的 type
print (transformStr (++ "!") ["foo", "bar", "baz"])
-- ["foo!","bar!","baz!"]
使用起來會像是這樣,我們傳入一個 function 是要串上 "!"
以及再傳入一個 list ["foo", "bar", "baz"]
。
還記得嗎我們第一個參數是 (String -> String)
那 (++ "!")
剛好也是 String -> String
,這邊就可以看出來 curried function 的偉大之處!!
然後我們還可以更進一步封裝起來
strAddExclamation :: [String] -> [String]
strAddExclamation = transformStr (++ "!")
print (strAddExclamation ["foo", "bar", "baz"])
-- ["foo!","bar!","baz!"]
其實我們剛剛的 transformStr
就有點像是 map
在做的事情,map
會傳入一個 function 及 List 然後讓 List 中的每個元素都執行那個 function 並回傳一個新的 List
:t map -- map :: (a -> b) -> [a] -> [b]
map (+1) [1,2,3] -- [2,3,4]
map (elem 'f') ["foo","bar","baz"] -- [True,False,False]
map (*2) [x*2 | x <-[1..5]] -- [4,8,12,16,20]
map (++"!") ["foo","bar","baz"] ["foo!","bar!","baz!"]
由此可知 map
在 Haskell 中十分強大的 function ,因為它可以用很優雅的方式讓我們將一個 a
型別的 List 轉換成 b
型別的 List
今天我們聊到了 FP 中非常重要且經常用到的概念,而且會很常不知不覺地就是在使用了像是我使用 js/ts 開發時蠻常會使用到 array.map
或者 array.filter
幫我處理 array,或者是利用 higher order function 來幫我們減少程式碼的撰寫,特別像是在寫前端時很常會需要傳遞 function 到 component props
<Component
onStateChange={(state)=>{
handleStateChange(state)
}}
/>
<Component
onStateChange={handleStateChange}
/>
今天的程式碼:https://github.com/toddLiao469469/30days-for-haskell